﻿using MTP.Utils;

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace MTP.Dispatch
{
    class DispatchServer : DispatchBase, IDispatchServer
    {
        private readonly MTPPackage defaultPackage = new MTPPackage(new byte[0], PackageType.ByteArray, null);

        private readonly SocketStream socketStream;

        private readonly ConcurrentDictionary<string, SocketStream> clients = new ConcurrentDictionary<string, SocketStream>();

        private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

        private readonly AddressFamily addressFamily;
        public Action<string, IDispatchServer> ConnectedEvent;

        public Action<string> DisconnectEvent;

        public Func<string, MTPPackage, Task<MTPPackage>> ReceiveEvent;

        public DispatchServer(byte[] cipher, AddressFamily addressFamily)
        {
            socketStream = new SocketStream(Util.GetSocket(addressFamily), cipher, null);
            this.addressFamily = addressFamily;
        }

        public IDispatchServer Run(int port, IPAddress iPAddress)
        {
            socketStream.Bind(this, cancellationTokenSource.Token, new IPEndPoint(iPAddress, port));
            return this;
        }

        public IDispatchServer Run(int port, string hostname)
        {
            if (!IPAddress.TryParse(hostname, out IPAddress address))
            {
                address = Dns.GetHostEntry(hostname).AddressList.First(a => a.AddressFamily == addressFamily);
            }
            return Run(port, address);
        }

        public IDispatchServer Run(int port)
        {
            return Run(port, IPAddress.Any);
        }

        public override void Receive(Guid guid, MTPPackage package, string clientId)
        {
            if (clients.TryGetValue(clientId, out var socket))
            {
                MTPPackage result;
                try
                {
                    result = ReceiveEvent?.Invoke(clientId, package)?.GetAwaiter().GetResult() ?? defaultPackage;
                }
                catch (Exception e)
                {
                    result = MTPPackage.BuilderErrorPackage(GetLast(e));
                }
                socket.Send(guid, result);
            }
        }


        private Exception GetLast(Exception e)
        {
            if (e.InnerException != null)
                return GetLast(e.InnerException);
            return e;
        }

        public override void Accept(SocketStream socket)
        {
            socket.RunListen(cancellationTokenSource.Token);
            clients.TryAdd(socket.ClientId, socket);
            ConnectedEvent?.Invoke(socket.ClientId, this);
        }



        public void Send(string clientId, MTPPackage package)
        {
            Guid guid = Guid.NewGuid();
            if (clients.TryGetValue(clientId, out var socket))
            {
                socket.Send(guid, package);
            }
            else
            {
                throw new IOException(" the connection has been disconnected.");
            }
        }

        public void SendAll(MTPPackage package)
        {
            Guid guid = Guid.NewGuid();
            foreach (var socket in clients.Values)
            {
                socket.Send(guid, package);
            }
        }

        public IDispatchServer OnReceive(Func<string, MTPPackage, Task<MTPPackage>> receive)
        {
            ReceiveEvent = receive;
            return this;
        }

        public IDispatchServer OnConnected(Action<string, IDispatchServer> connected)
        {
            ConnectedEvent = connected;
            return this;
        }

        public IDispatchServer OnDisconnect(Action<string> disconnect)
        {
            DisconnectEvent = disconnect;
            return this;
        }

        public override void Dispose(string clientId)
        {
            lock (this)
            {
                if (clients.TryRemove(clientId, out var socket))
                {
                    socket.Dispose();
                    DisconnectEvent?.Invoke(clientId);
                }
            }
        }


        public void Dispose()
        {
            foreach (var clintKey in clients.Keys)
            {
                Dispose(clintKey);
            }
            cancellationTokenSource.Cancel();
            cancellationTokenSource.Dispose();
            socketStream.Dispose();
        }

        public void CloseSync()
        {
            cancellationTokenSource.Token.WaitHandle.WaitOne();
        }

    }
}
